/* * Copyright 2016 Google Inc. * * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ #include "SampleCode.h" #include "SkCanvas.h" #include "SkLightingShader.h" #include "SkNormalSource.h" #include "sk_tool_utils.h" class ParentControl; // Abstract base class for all components that a control panel must have class Control : public SkRefCnt { public: Control(SkString name) : fName(name) , fParent(nullptr) , fRelativePos(SkPoint::Make(0.0f, 0.0f)) {} // Use this to propagate a click's position down to a control. Gets modulated by the component's // relative position bool click(const SkPoint& clickPos) { SkPoint relativeClickPos = SkPoint::Make(clickPos.fX - fRelativePos.fX, clickPos.fY - fRelativePos.fY); return this->onClick(relativeClickPos); } // Use this to draw the control and its appropriate children. Gets modulated by the component's // relative position. void drawContent(SkCanvas *canvas) { canvas->save(); canvas->translate(fRelativePos.fX, fRelativePos.fY); this->onDrawContent(canvas); canvas->restore(); } /* Returns true when click position argumend lands over a control region in this control. Click * position gets modulated by the component's relative position. * * @param click The position of the click in the coordinate space relative to the parent */ bool isInCtrlRegion(const SkPoint& click) { SkPoint relativeClickPos = SkPoint::Make(click.fX - fRelativePos.fX, click.fY - fRelativePos.fY); return this->onIsInCtrlRegion(relativeClickPos); } // Returns height of content drawn virtual SkScalar height() const = 0; // Sets the parent of this component. May only be used once. Height must remain constant after // parent is set. void setParent(ParentControl *parent, const SkPoint& relativePos) { SkASSERT(parent); SkASSERT(!fParent); // No chidren transfer since relativeY would get invalid for younger kid fParent = parent; fRelativePos = relativePos; this->onSetParent(); } // Overriden by sub-classes that need to recompute fields after parent is set. Called after // setting fParent. virtual void onSetParent() {} // Overriden by sub-classes that need to know when a click is released. virtual void onClickRelease() {} protected: // Draws a label for the component, using its name and a passed value. Does NOT modulate by // relative height, expects CTM to have been adjusted in advance. void drawLabel(SkCanvas *canvas, const SkString& valueStr) const { // TODO Cache this sk_sp<SkTypeface> fLabelTypeface = sk_tool_utils::create_portable_typeface("sans-serif", SkFontStyle()); SkString label; label.append(fName); label.append(": "); label.append(valueStr); SkPaint labelPaint; labelPaint.setTypeface(fLabelTypeface); labelPaint.setAntiAlias(true); labelPaint.setColor(0xFFFFFFFF); labelPaint.setTextSize(12.0f); canvas->drawText(label.c_str(), label.size(), 0, kLabelHeight - 6.0f, labelPaint); } SkString fName; ParentControl* fParent; static constexpr SkScalar kLabelHeight = 20.0f; private: // Overriden by sub-class to draw component. Do not call directly, drawContent() modulates by // relative position. virtual void onDrawContent(SkCanvas *canvas) = 0; // Overriden by sub-class to handle clicks. Do not call directly, click() modulates by relative // position. Return true if holding mouse capture virtual bool onClick(const SkPoint& clickPos) { return false; } // Overriden by sub-classes with controls. Should return true if clickPos lands inside a control // region, to enable mouse caputre. virtual bool onIsInCtrlRegion(const SkPoint& clickPos) const { return false; } // The position of the control relative to it's parent SkPoint fRelativePos; }; class ParentControl : public Control { // Interface for all controls that have children public: ParentControl(const SkString& name) : INHERITED(name) {} // Adds a child virtual void add(sk_sp<Control> control) = 0; // Returns the control's width. Used to propagate width down to components that don't specify it virtual SkScalar width() const = 0; private: typedef Control INHERITED; }; class ControlPanel : public ParentControl { public: ControlPanel(SkScalar width) : ParentControl(SkString("ControlPanel")) , fWidth(width) , fHeight(0.0f) , fSelectedControl(-1) {} // Width unspecified, expectation is inheritance from parent ControlPanel() : ControlPanel(-1.0f) {} // Use this for introducing clicks on a ControlPanel from outside of the framework. It // propagates click release or position down the chain. Returns false when click capture is // being released. bool inClick(SkView::Click *inClick) { if (SkView::Click::State::kUp_State == inClick->fState) { this->onClickRelease(); return false; } return this->click(inClick->fCurr); } // Add children void add(sk_sp<Control> control) override { SkASSERT(!fParent); // Validity of parent's relativeY and fHeight depends on immutability fControls.push_back(control); control->setParent(this, SkPoint::Make(0.0f, fHeight)); fHeight += control->height(); } SkScalar width() const override { return fParent ? fParent->width() : fWidth; // Width inherited from parent if there is one } SkScalar height() const override { return fHeight; } // Propagate click release to selected control, deselect control void onClickRelease() override { if (fSelectedControl >= 0) { fControls[fSelectedControl]->onClickRelease(); } fSelectedControl = -1; } // Propagate onSetParent() down to children, some might need fParent->width() refresh void onSetParent() override { for (int i = 0; i < fControls.count(); i++) { fControls[i]->onSetParent(); } } // Holds a vertical shelf of controls. Can't be hierarchy root if not given a width value. static sk_sp<ParentControl> Make() { return sk_sp<ParentControl>(new ControlPanel()); } // Holds a vertical shelf of controls. Only control that can be hooked from outside the // framework. static sk_sp<ParentControl> Make(SkScalar width) { return sk_sp<ParentControl>(new ControlPanel(width)); } protected: // Returns true if control panel has mouse captured, false when it is ready to release // capture bool onClick(const SkPoint& click) override { if (fSelectedControl == -1) { // If no child control selected, check every child for (int i = 0; i < fControls.count(); i++) { if (fControls[i]->isInCtrlRegion(click)) { fSelectedControl = i; break; } } } if (fSelectedControl >= 0) { // If child control selected, propagate click bool keepSelection = fControls[fSelectedControl]->click(click); if (!keepSelection) { fSelectedControl = -1; } return keepSelection; } return false; } // Draw all children void onDrawContent(SkCanvas* canvas) override { canvas->save(); for (int i = 0; i < fControls.count(); i++) { fControls[i]->drawContent(canvas); } canvas->restore(); } // Check all children's control regions bool onIsInCtrlRegion(const SkPoint& clickPos) const override { for (int i = 0; i < fControls.count(); i++) { if (fControls[i]->isInCtrlRegion(clickPos)) { return true; } } return false; } private: SkScalar fWidth; SkScalar fHeight; SkTArray<sk_sp<Control>> fControls; int fSelectedControl; }; class DiscreteSliderControl : public Control { public: SkScalar height() const override { return 2.0f * kLabelHeight; } // Set width-dependant variables when new parent is set void onSetParent() override { fCtrlRegion = SkRect::MakeXYWH(0.0f, kLabelHeight, fParent->width(), kSliderHeight); fSliderRange = fParent->width() - kSliderWidth; } /* Make a slider for an integer value. Snaps to discrete positions. * * @params name The name of the control, displayed in the label * @params output Pointer to the integer that will be set by the slider * @params min Min value for output. * @params max Max value for output. */ static sk_sp<Control> Make(SkString name, int* output, int min, int max) { return sk_sp<Control>(new DiscreteSliderControl(name, output, min, max)); } protected: void onDrawContent(SkCanvas* canvas) override { SkASSERT(fParent); int numChoices = fMax - fMin + 1; fSlider.offsetTo(fSliderRange * ( (*fOutput)/SkIntToScalar(numChoices) + 1.0f/(2.0f * numChoices) ), fSlider.fTop); SkString valueStr; valueStr.appendS32(*fOutput); this->drawLabel(canvas, valueStr); SkPaint sliderPaint; sliderPaint.setColor(0xFFF3F3F3); canvas->drawRect(fSlider, sliderPaint); SkPaint ctrlRegionPaint; ctrlRegionPaint.setColor(0xFFFFFFFF); ctrlRegionPaint.setStyle(SkPaint::kStroke_Style); ctrlRegionPaint.setStrokeWidth(2.0f); canvas->drawRect(fCtrlRegion, ctrlRegionPaint); } bool onClick(const SkPoint& clickPos) override { SkASSERT(fParent); SkScalar x = SkScalarPin(clickPos.fX, 0.0f, fSliderRange); int numChoices = fMax - fMin + 1; *fOutput = SkTMin(SkScalarFloorToInt(numChoices * x / fSliderRange) + fMin, fMax); return true; } bool onIsInCtrlRegion(const SkPoint& clickPos) const override { SkASSERT(fParent); return fCtrlRegion.contains(SkRect::MakeXYWH(clickPos.fX, clickPos.fY, 1, 1)); } private: DiscreteSliderControl(SkString name, int* output, int min, int max) : INHERITED(name) , fOutput(output) , fMin(min) , fMax(max) { fSlider = SkRect::MakeXYWH(0, kLabelHeight, kSliderWidth, kSliderHeight); } int* fOutput; int fMin; int fMax; SkRect fSlider; // The rectangle that slides // The region in which the rectangle slides. Also the region in which mouse is caputred SkRect fCtrlRegion; SkScalar fSliderRange; // The width in pixels over which the slider can slide static constexpr SkScalar kSliderHeight = 20.0f; static constexpr SkScalar kSliderWidth = 10.0f; typedef Control INHERITED; }; class ControlSwitcher : public ParentControl { public: // Add children void add(sk_sp<Control> control) override { SkASSERT(!fParent); // Validity of parent's relativeY and fHeight depends on immutability fControls.push_back(control); control->setParent(this, SkPoint::Make(0.0f, kSelectorHeight)); fHeight = SkMaxScalar(fHeight, control->height()); // Setting height to max child height. } SkScalar width() const override { return fParent ? (fParent->width()) : 0; } SkScalar height() const override { return fHeight; } // Propagate onClickRelease to control that currently captures mouse void onClickRelease() override { if (fCtrlOnClick) { fCtrlOnClick->onClickRelease(); } fCtrlOnClick = nullptr; } void onSetParent() override { for (int i = 0; i < fControls.count(); i++) { fControls[i]->onSetParent(); // Propagate to children } // Finalize control selector // TODO can be moved to constructor if list-initialized if (!finalizedChildren) { fControlSelector = DiscreteSliderControl::Make( SkString(fName), &fSelectedControl, 0, fControls.count()-1); fControlSelector->setParent(this, SkPoint::Make(0.0f, 0.0f)); fHeight += kSelectorHeight; SkASSERT(fControlSelector->height() <= kSelectorHeight); } } /* A set of a selector and a list of controls. Displays the control from the list of controls * with the index set by the aforementioned selector. * * @param name The name of the switcher. Will be displayed in the selector's label. */ static sk_sp<ParentControl> Make(const SkString& name) { return sk_sp<ParentControl>(new ControlSwitcher(name)); } protected: // Draw selector and currently selected control void onDrawContent(SkCanvas* canvas) override { fControlSelector->drawContent(canvas); fControls[fSelectedControl]->drawContent(canvas); } // Returns true if control panel has mouse captured, false when it is ready to release // capture bool onClick(const SkPoint& click) override { if (!fCtrlOnClick) { if (fControlSelector->isInCtrlRegion(click)) { fCtrlOnClick = fControlSelector.get(); } else if (fControls[fSelectedControl]->isInCtrlRegion(click)) { fCtrlOnClick = fControls[fSelectedControl].get(); } } if (fCtrlOnClick) { return fCtrlOnClick->click(click); } return false; } // Is in control region of selector or currently selected control bool onIsInCtrlRegion(const SkPoint& clickPos) const override { if (fControlSelector->isInCtrlRegion(clickPos)) { return true; } if (fControls[fSelectedControl]->isInCtrlRegion(clickPos)) { return true; } return false; } private: ControlSwitcher(const SkString& name) : INHERITED(name) , fHeight(0.0) , fSelectedControl(0) , fCtrlOnClick(nullptr){} bool finalizedChildren = false; sk_sp<Control> fControlSelector; SkScalar fHeight; SkTArray<sk_sp<Control>> fControls; int fSelectedControl; Control* fCtrlOnClick; static constexpr SkScalar kSelectorHeight = 40.0f; typedef ParentControl INHERITED; }; class ContinuousSliderControl : public Control { public: SkScalar height() const override { return 2.0f * kLabelHeight; } void onSetParent() override { fSlider = SkRect::MakeXYWH(0, kLabelHeight, kSliderWidth, kSliderHeight); fCtrlRegion = SkRect::MakeXYWH(0.0f, kLabelHeight, fParent->width(), kSliderHeight); fSliderRange = fParent->width() - kSliderWidth; } /* Make a slider for an SkScalar. * * @params name The name of the control, displayed in the label * @params output Pointer to the SkScalar that will be set by the slider * @params min Min value for output * @params max Max value for output */ static sk_sp<Control> Make(const SkString& name, SkScalar* output, SkScalar min, SkScalar max) { return sk_sp<Control>(new ContinuousSliderControl(name, output, min, max)); } protected: void onDrawContent(SkCanvas* canvas) override { SkASSERT(fParent); SkScalar x = fSliderRange * (*fOutput - fMin) / (fMax - fMin); fSlider.offsetTo(SkScalarPin(x, 0.0f, fSliderRange), fSlider.fTop); SkString valueStr; valueStr.appendScalar(*fOutput); this->drawLabel(canvas, valueStr); SkPaint sliderPaint; sliderPaint.setColor(0xFFF3F3F3); canvas->drawRect(fSlider, sliderPaint); SkPaint ctrlRegionPaint; ctrlRegionPaint.setColor(0xFFFFFFFF); ctrlRegionPaint.setStyle(SkPaint::kStroke_Style); ctrlRegionPaint.setStrokeWidth(2.0f); canvas->drawRect(fCtrlRegion, ctrlRegionPaint); } bool onClick(const SkPoint& clickPos) override { SkASSERT(fParent); SkScalar x = SkScalarPin(clickPos.fX, 0.0f, fSliderRange); *fOutput = (x/fSliderRange) * (fMax - fMin) + fMin; return true; } bool onIsInCtrlRegion(const SkPoint& clickPos) const override { SkASSERT(fParent); return fCtrlRegion.contains(SkRect::MakeXYWH(clickPos.fX, clickPos.fY, 1, 1)); } private: ContinuousSliderControl(const SkString& name, SkScalar* output, SkScalar min, SkScalar max) : INHERITED(name) , fOutput(output) , fMin(min) , fMax(max) {} SkScalar* fOutput; SkScalar fMin; SkScalar fMax; SkRect fSlider; SkRect fCtrlRegion; SkScalar fSliderRange; static constexpr SkScalar kSliderHeight = 20.0f; static constexpr SkScalar kSliderWidth = 10.0f; typedef Control INHERITED; }; class RadialDirectionControl : public Control { public: SkScalar height() const override { return kLabelHeight + 2.0f * kRegionRadius; } /* Make a direction selector. * * @params name The name of the control, displayed in the label * @params output Pointer to the SkVector that will be set by the slider */ static sk_sp<Control> Make(const SkString& name, SkVector* output) { return sk_sp<Control>(new RadialDirectionControl(name, output)); } protected: void onDrawContent(SkCanvas* canvas) override { SkASSERT(fParent); SkString valueStr; valueStr.appendf("%.2f, %.2f", fOutput->fX, fOutput->fY); this->drawLabel(canvas, valueStr); SkPoint lineEnd = SkPoint::Make(fCtrlRegion.centerX(), fCtrlRegion.centerY()) + (*fOutput * (kRegionRadius - kCapRadius)); SkPaint linePaint; linePaint.setColor(0xFFF3F3F3); linePaint.setStrokeWidth(kStrokeWidth); linePaint.setAntiAlias(true); linePaint.setStrokeCap(SkPaint::kRound_Cap); canvas->drawLine(fCtrlRegion.centerX(), fCtrlRegion.centerY(), lineEnd.fX, lineEnd.fY, linePaint); SkPaint ctrlRegionPaint; ctrlRegionPaint.setColor(0xFFFFFFFF); ctrlRegionPaint.setStyle(SkPaint::kStroke_Style); ctrlRegionPaint.setStrokeWidth(2.0f); ctrlRegionPaint.setAntiAlias(true); canvas->drawCircle(fCtrlRegion.centerX(), fCtrlRegion.centerY(), kRegionRadius, ctrlRegionPaint); } bool onClick(const SkPoint& clickPos) override { SkASSERT(fParent); fOutput->fX = clickPos.fX - fCtrlRegion.centerX(); fOutput->fY = clickPos.fY - fCtrlRegion.centerY(); fOutput->normalize(); return true; } bool onIsInCtrlRegion(const SkPoint& clickPos) const override { SkASSERT(fParent); return fCtrlRegion.contains(SkRect::MakeXYWH(clickPos.fX, clickPos.fY, 1, 1)); } private: RadialDirectionControl(const SkString& name, SkVector* output) : INHERITED(name) , fOutput(output) { fCtrlRegion = SkRect::MakeXYWH(0.0f, kLabelHeight, kRegionRadius * 2.0f, kRegionRadius * 2.0f); } SkVector* fOutput; SkRect fCtrlRegion; static constexpr SkScalar kRegionRadius = 50.0f; static constexpr SkScalar kStrokeWidth = 6.0f; static constexpr SkScalar kCapRadius = kStrokeWidth / 2.0f; typedef Control INHERITED; }; class ColorDisplay: public Control { public: SkScalar height() const override { return kHeight; } void onSetParent() override { fDisplayRect = SkRect::MakeXYWH(0.0f, kPadding, fParent->width(), kHeight - kPadding); } /* Make a display that shows an SkColor3f. * * @params output Pointer to the SkColor3f that will be displayed */ static sk_sp<Control> Make(SkColor3f* input) { return sk_sp<Control>(new ColorDisplay(SkString("ColorDisplay"), input)); } protected: void onDrawContent(SkCanvas* canvas) override { SkASSERT(fParent); SkPaint displayPaint; displayPaint.setColor(SkColor4f::FromColor3f(*fInput, 1.0f).toSkColor()); canvas->drawRect(fDisplayRect, displayPaint); } private: ColorDisplay(const SkString& name, SkColor3f* input) : INHERITED(name) , fInput(input) {} SkColor3f* fInput; SkRect fDisplayRect; static constexpr SkScalar kHeight = 24.0f; static constexpr SkScalar kPadding = 4.0f; typedef Control INHERITED; }; class BevelView : public SampleView { public: BevelView() : fShapeBounds(SkRect::MakeWH(kShapeBoundsSize, kShapeBoundsSize)) , fControlPanel(kCtrlRange) { this->setBGColor(0xFF666868); // Slightly colorized gray for contrast // Controls fBevelWidth = 25.0f; fBevelHeight = 25.0f; fBevelType = 0; int currLight = 0; fLightDefs[currLight++] = {SkVector::Make(0.0f, 1.0f), 1.0f, SkColor3f::Make(0.6f, 0.45f, 0.3f)}; fLightDefs[currLight++] = {SkVector::Make(0.0f, -1.0f), 1.0f, SkColor3f::Make(0.3f, 0.45f, 0.6f)}; fLightDefs[currLight++] = {SkVector::Make(1.0f, 0.0f), 1.0f, SkColor3f::Make(0.0f, 0.0f, 0.0f)}; // Making sure we initialized all lights SkASSERT(currLight == kNumLights); fControlPanel.add(ContinuousSliderControl::Make(SkString("BevelWidth"), &fBevelWidth, 1.0f, kShapeBoundsSize)); fControlPanel.add(ContinuousSliderControl::Make(SkString("BevelHeight"), &fBevelHeight, -50.0f, 50.0f)); fControlPanel.add(DiscreteSliderControl::Make(SkString("BevelType"), &fBevelType, 0, 2)); sk_sp<ParentControl> lightCtrlSelector = ControlSwitcher::Make(SkString("SelectedLight")); for (int i = 0; i < kNumLights; i++) { SkString name("Light"); name.appendS32(i); sk_sp<ParentControl> currLightPanel = ControlPanel::Make(); SkString dirName(name); dirName.append("Dir"); currLightPanel->add(RadialDirectionControl::Make(dirName, &(fLightDefs[i].fDirXY))); SkString heightName(name); heightName.append("Height"); currLightPanel->add(ContinuousSliderControl::Make(heightName, &(fLightDefs[i].fDirZ), 0.0f, 2.0f)); SkString redName(name); redName.append("Red"); currLightPanel->add(ContinuousSliderControl::Make(redName, &(fLightDefs[i].fColor.fX), 0.0f, 1.0f)); SkString greenName(name); greenName.append("Green"); currLightPanel->add(ContinuousSliderControl::Make(greenName, &(fLightDefs[i].fColor.fY), 0.0f, 1.0f)); SkString blueName(name); blueName.append("Blue"); currLightPanel->add(ContinuousSliderControl::Make(blueName, &(fLightDefs[i].fColor.fZ), 0.0f, 1.0f)); currLightPanel->add(ColorDisplay::Make(&(fLightDefs[i].fColor))); lightCtrlSelector->add(currLightPanel); } fControlPanel.add(lightCtrlSelector); fControlPanelSelected = false; fDirtyNormalSource = true; fLabelTypeface = sk_tool_utils::create_portable_typeface("sans-serif", SkFontStyle()); } protected: bool onQuery(SkEvent *evt) override { if (SampleCode::TitleQ(*evt)) { SampleCode::TitleR(evt, "Bevel"); return true; } return this->INHERITED::onQuery(evt); } enum Shape { kCircle_Shape, kRect_Shape, }; void drawShape(enum Shape shape, SkCanvas* canvas) { canvas->save(); SkPaint paint; if (fDirtyNormalSource) { fNormalSource = SkNormalSource::MakeBevel((SkNormalSource::BevelType)fBevelType, fBevelWidth, fBevelHeight); fDirtyNormalSource = false; } paint.setShader(SkLightingShader::Make(nullptr, fNormalSource, fLights)); paint.setAntiAlias(true); paint.setColor(0xFFDDDDDD); switch (shape) { case kCircle_Shape: canvas->drawCircle(fShapeBounds.centerX(), fShapeBounds.centerY(), fShapeBounds.width()/2.0f, paint); break; case kRect_Shape: canvas->drawRect(fShapeBounds, paint); break; default: SkDEBUGFAIL("Invalid shape enum for drawShape"); } canvas->restore(); } void onDrawContent(SkCanvas *canvas) override { canvas->save(); canvas->resetMatrix(); // Force static control panel position fControlPanel.drawContent(canvas); canvas->restore(); SkLights::Builder builder; for (int i = 0; i < kNumLights; i++) { builder.add(SkLights::Light::MakeDirectional(fLightDefs[i].fColor, SkPoint3::Make(fLightDefs[i].fDirXY.fX, fLightDefs[i].fDirXY.fY, fLightDefs[i].fDirZ))); } builder.setAmbientLightColor(SkColor3f::Make(0.4f, 0.4f, 0.4f)); fLights = builder.finish(); // Draw shapes SkScalar xPos = kCtrlRange + 25.0f; SkScalar yPos = fShapeBounds.height(); for (Shape shape : { kCircle_Shape, kRect_Shape }) { canvas->save(); canvas->translate(xPos, yPos); this->drawShape(shape, canvas); canvas->restore(); xPos += 1.2f * fShapeBounds.width(); } } SkView::Click* onFindClickHandler(SkScalar x, SkScalar y, unsigned modi) override { return new SkView::Click(this); } bool onClick(Click *click) override { // Control panel mouse handling fControlPanelSelected = fControlPanel.inClick(click); if (fControlPanelSelected) { // Control modification fDirtyNormalSource = true; this->inval(nullptr); return true; } // TODO move shapes this->inval(nullptr); return true; } private: static constexpr int kNumTestRects = 3; static constexpr SkScalar kShapeBoundsSize = 120.0f; static constexpr SkScalar kCtrlRange = 150.0f; static constexpr int kNumLights = 3; const SkRect fShapeBounds; SkScalar fBevelWidth; SkScalar fBevelHeight; int fBevelType; sk_sp<SkNormalSource> fNormalSource; bool fDirtyNormalSource; sk_sp<SkLights> fLights; struct LightDef { SkVector fDirXY; SkScalar fDirZ; SkColor3f fColor; LightDef() {} LightDef(SkVector dirXY, SkScalar dirZ, SkColor3f color) : fDirXY(dirXY) , fDirZ(dirZ) , fColor(color) {} }; LightDef fLightDefs[kNumLights]; ControlPanel fControlPanel; bool fControlPanelSelected; sk_sp<SkTypeface> fLabelTypeface; typedef SampleView INHERITED; }; ////////////////////////////////////////////////////////////////////////////// static SkView* MyFactory() { return new BevelView; } static SkViewRegister reg(MyFactory);